/*
###################################################################
#
#		Name: 		SMC Spray Manager
#		Author:		Itsalati
#		Web:		http://www.soloistsmodcentral.com
#		Purpose:	Manages in-game sprays. Ban individual clients from
#					using sprays for any length of time. Database support
#					for bans
#			
###################################################################
*/
#pragma semicolon 1
















#define PLUGIN_VERSION "1.0"
#define CONNECTION_CONFIG "smc_spraymanager"

#define __DEBUG

#include <sourcemod>
#include <adminmenu>
#include <smlib>
#include <sdktools>

#include <itsa_useful>
#include <itsa_debug>

#include <smc_spraymanager>


#define MODTAG "[SprayMan] "

public Plugin:myinfo =
{
	name = "SMC Spray Manager",
	description = "Displays spray owners, removes targetted sprays",
	author = "Itsalati",
	version = PLUGIN_VERSION,
	url = "https://soloistsmodcentral.com"
}


#define SPRAYFUDGEAMOUNT 34.0

#define MAXBANCACHEPAGES 5
#define MAX_CACHE_SIZE MAXBANCACHEPAGES * 10

#define BANLENGTHPERMA 0

#define HUDMSG_POSX -1.0
#define HUDMSG_POSY 0.7 
#define HUDMSG_RED 105
#define HUDMSG_GREEN 121
#define HUDMSG_BLUE 133
#define HUDMSG_ALPHA 255

// Database version. Increment this when database structure
// is altered to prompt the update sql to be ran before anything else.
#define SQL_DATABASE_VERSION 1


//-------------------------------------------
//			GLOBALS
//-------------------------------------------

//	*** HANDLES ***
new Handle:g_timerShowSprayDetails 	= INVALID_HANDLE;
new Handle:g_hSQL 					= INVALID_HANDLE;	// Handle to the SQL Database connection.
new Handle:g_hGUI 					= INVALID_HANDLE;	// Handle to the HUD Synchroniser.

new Handle:g_timerQueryQueueProc 	= INVALID_HANDLE;

//	*** CONVARS ***
new Handle:cvEnabled 				= INVALID_HANDLE;
new Handle:cvTimerDelay				= INVALID_HANDLE;
new Handle:cvSprayDistance 			= INVALID_HANDLE;
new Handle:cvDoDisplay 				= INVALID_HANDLE;
new Handle:cvDisplayMode 			= INVALID_HANDLE;

new Handle:cvSQLEnabled				= INVALID_HANDLE;
new Handle:cvSQLLastError 			= INVALID_HANDLE;

new Handle:cvSQLMaxAttempts 		= INVALID_HANDLE;
new Handle:cvSQLRetryDelay			= INVALID_HANDLE;

new Handle:cvSQLQueueProcRate 		= INVALID_HANDLE;
new Handle:cvSQLQueryMaxAtmps		= INVALID_HANDLE;

//	*** VARIABLES ***
new bool:  g_bEnabled = false;

new bool:  g_bHasSprayed[MAXPLAYERS+1];			// Stores if player has made a spray on this map.
new bool:  g_bSprayBanned[MAXPLAYERS+1];		// Stores if player is banned from making spray.
new bool:  g_bSprayProtected[MAXPLAYERS+1];		// Stores if player is protected from being sprayed over.

new bool:  g_bLateLoad;							// Is plugin late-loaded?

new bool:  g_bSQLEnabled 			= false;	// Is SQL in an enabled state?
new bool:  g_bSQLReady 				= false;	// SQL has completed all actions related to initialisation. (Created tables)
new 	   g_iSQLNumConnectAttempts; 			// The number of attempts currently made on connecting to the database.

new Float: g_flSprayLocs[MAXPLAYERS][3];		// Stores locations of each players spray decal.
new Float: g_flSprayNormals[MAXPLAYERS][3];		// The hit normal of a clients spray 


//	*** BAN CACHE ***
new CSprayBanInfo:g_aBanCache[MAX_CACHE_SIZE];

//	*** BAN MENUS ***
new        g_iTotalRecords 			= 0;
new        g_iItemsPerPage 			= 0;
new        g_iMenuSelectionPosition[MAXPLAYERS];

new CSprayMenuPending:g_clientsPendingMenu[MAXPLAYERS][csmpSize];


// 	*** QUERY QUEUE ***
new Handle:g_adtQueryQueue = INVALID_HANDLE;
new 				g_iQueuePos = 0;

/* Query cache contents
*
* CSprayQueryInfo:
* 	- bRunning
* 	- nAttemptCount
* 	- hQueryData (datapack)
*		- callingClient
* 		- banInfo
*/
















//-------------------------------------------
//
//
//			SOURCEMOD
//
//
//-------------------------------------------
















public APLRes:AskPluginLoad2( Handle:thisplugin, bool:bLateLoad, String:error[] )
{
	g_bLateLoad = bLateLoad;

	return APLRes_Success;
}



public OnPluginStart()
{
	Debug_OpenLog( "addons/sourcemod/logs/smc_spraymanager_debug.log" );

	LoadTranslations( "common.phrases" );

	AutoExecConfig();

	AddTempEntHook( "Player Decal", hookPlayerDecal );

	RegAdminCmd( "sm_rspray",		cmdRemoveSpray,	ADMFLAG_BAN );
	RegAdminCmd( "sm_sban",			cmdSprayBan,	ADMFLAG_BAN );
	RegAdminCmd( "sm_sunban",		cmdSprayUnBan,	ADMFLAG_BAN );
	RegAdminCmd( "sm_spraybans",	cmdShowBanList, ADMFLAG_BAN );

	RegAdminCmd( "sm_spray", 		cmdMakeSpray, 	ADMFLAG_ROOT );
	RegAdminCmd( "sm_sp",			cmdMakeSpray, 	ADMFLAG_ROOT );

	RegAdminCmd( "sm_unlock",		cmdUnlockDB,	ADMFLAG_ROOT );

	cvEnabled 		 = CreateConVar( "sm_sprayman_enabled", "1", "Disable plugin functions. Halts timers.", 0, true, 0.0, true, 1.0 );
	cvTimerDelay 	 = CreateConVar( "sm_sprayman_timerdelay", "1.5", "Delay between checking players aim for sprays", 0, true, 0.1 );
	cvSprayDistance	 = CreateConVar( "sm_sprayman_distance", "160.0", "Max distance in units/inches from a spray a client can be spray owner name to be displayed.", 0, true, 0.1 );
	cvDisplayMode	 = CreateConVar( "sm_sprayman_displaymode", "3", "Method to display names on hud with. 0 = csay, 1 = screen-right hint, 2 = center hint, 3 = HUD Text (not supported by all games)", 0, true, 0.0, true, 3.0 );
	cvDoDisplay 	 = CreateConVar( "sm_sprayman_display", "1", "Should display names to players.", 0, true, 0.0, true, 1.0 );

	cvSQLEnabled	 = CreateConVar( "sm_sprayman_sql_enabled", "1", "Should bans be stored and loaded via SQL?", 0, true, 0.0, true, 1.0 );
	cvSQLLastError   = CreateConVar( "sm_sprayman_sql_lasterror", "", "If not empty, contains the SQL error that caused it to fail." );

	cvSQLMaxAttempts = CreateConVar( "sm_sprayman_sql_maxattempts", "4", "Number of times to attempt connecting to database before failing.", 0, true, 0.0 );
	cvSQLRetryDelay  = CreateConVar( "sm_sprayman_sql_retrydelay", "2.0", "Number of seconds before retrying a connection.", 0, true, 0.1 );

	cvSQLQueueProcRate = CreateConVar( "sm_sprayman_sql_queryprocessrate", "1000", "How fast in milliseconds to poll the query queue.", 0, true, 100.0, true, 300000.0 );
	cvSQLQueryMaxAtmps = CreateConVar( "sm_sprayman_sql_querymaxattemps", "3", "How many times to repeat a query before removing it from the queue.", 0, true, 0.0 );

	HookConVarChange( cvEnabled, 		cvEnabledChanged );
	HookConVarChange( cvTimerDelay, 	cvTimerDelayChanged );
	HookConVarChange( cvDisplayMode, 	cvDisplayModeChanged );
	HookConVarChange( cvSQLEnabled,		cvSQLEnabledChanged );
	HookConVarChange( cvSQLQueueProcRate, cvSQLQueueProcRateChanged );
/*
	new Handle:tempMenu = CreateMenu(BanMenuHandler);
	g_iItemsPerPage = GetMaxPageItems( GetMenuStyle(tempMenu) ) - 3;
	CloseHandle(tempMenu);
*/
}



public OnConfigsExecuted()
{
	if ( GetConVarBool(cvEnabled) ) {
		LogMessage( "NOTICE:\tInitialising Spray Manager v%s", PLUGIN_VERSION );
		SPRAY_Initialise();
	}
	else {
		LogMessage( "NOTICE:\tSpray Manager v%s is loaded but NOT enabled.", PLUGIN_VERSION );
	}
}



public OnPluginEnd()
{
	SPRAY_Uninitialise();

	if ( g_hSQL != INVALID_HANDLE )
		CloseHandle( g_hSQL );
}



public OnClientAuthorized( client )
{
	LogMessage( "NOTICE:\tclient %N connected", client );
	g_bHasSprayed[client] = false;
	g_flSprayLocs[client] = Float:{0.0, 0.0, 0.0};

	decl String:sAuth[MAX_AUTH_LENGTH];
	GetClientAuthString( client, sAuth, MAX_AUTH_LENGTH );


	LogMessage( "NOTICE:\tchecking if client %N auth %s exists in database", client, sAuth );
	SPRAY_SQLEnqueueCheckOnConnect( client, sAuth );
}



/********************************************
* Callback for when sm_sprayman_timerdelay changes. Forces the timer to trigger.
* Then kills it and creates a new one with the new delay.
*/
public cvTimerDelayChanged( Handle:self, const String:oldValue[], const String:newValue[] )
{
	new Float:flDelay = StringToFloat(newValue);

	TriggerTimer( g_timerShowSprayDetails, true );
	KillTimer( g_timerShowSprayDetails );
	g_timerShowSprayDetails = INVALID_HANDLE;
	g_timerShowSprayDetails = CreateTimer( flDelay, timerShowSprayDetails, INVALID_HANDLE, TIMER_REPEAT );
}



/********************************************
* Callback for when sm_sprayman_enabled changes. Disables timers.
* Sets g_bEnabled to false.
*/
public cvEnabledChanged( Handle:self, const String:oldValue[], const String:newValue[] )
{
	new bool:bEnabled = bool:StringToInt(newValue);

	g_bEnabled = bEnabled;

	// If we are enabled, run through initialise.
	// Else, kill timers and handles, leave database because it's not hurting anyone.
	if ( bEnabled ) {
		SPRAY_Initialise();
	} else {
		SPRAY_Uninitialise();
	}
}



/********************************************
* Callback for when sm_sprayman_displaymode changes. Used to check supported features.
*/
public cvDisplayModeChanged( Handle:self, const String:oldValue[], const String:newValue[] )
{
	new iNewMode = StringToInt(newValue);

	if ( iNewMode == 4 && g_hGUI == INVALID_HANDLE ) {
		g_hGUI = CreateHudSynchronizer();
		if ( g_hGUI == INVALID_HANDLE ) 
			SetConVarInt( cvDisplayMode, 0 );
	}
}



/********************************************
* Callback for when sm_sprayman_usesqlban changes. Used to connect to database if necessary and enable it's use.
*/
public cvSQLEnabledChanged( Handle:self, const String:oldValue[], const String:newValue[] )
{
	new bool:bEnabled = bool:StringToInt(newValue);

	g_bSQLEnabled = bEnabled;
	if (bEnabled )
		SPRAY_SQLConnect();
	else 
		SPRAY_SQLDisconnect();
}


public cvSQLQueueProcRateChanged( Handle: self, const String:oldValue[], const String:newValue[] )
{
	new Float:newInterval = StringToFloat(newValue);

	if ( g_timerQueryQueueProc != INVALID_HANDLE ) {
		TriggerTimer( g_timerQueryQueueProc, true );
		KillTimer( g_timerQueryQueueProc );
		g_timerQueryQueueProc = CreateTimer( newInterval * 0.001, timerProcessQueryQueue, INVALID_HANDLE, TIMER_REPEAT );
	}
}


/********************************************
* Called every time g_timerShowSprayDetails triggers.
*/
public Action:timerShowSprayDetails( Handle:self )
{
	// Debug_PrintToLog( false, "timerShowSprayDetails:" );
	// This should never happen but just in case.
	if ( !g_bEnabled )
		return Plugin_Stop;

	if ( !GetConVarBool(cvDoDisplay) )
		return Plugin_Handled;

	for ( new i=1; i<=MaxClients; i++ ) {
		//Debug_PrintToLog( false, "\tClient: %i", i );
		if ( !Client_IsValid(i) || !Client_IsIngame(i) )
			continue;

		new target;
		if ( (target = SPRAY_CalculateSprayVisible(i)) > 0 ) 
			SPRAY_DisplaySprayName( i, target );
	}
	return Plugin_Handled;
}



/********************************************
* Called when a player creates a spray decal.
*/
public Action:hookPlayerDecal( const String:te_name[], const clients[], numClients, Float:delay )
{
	if ( !g_bEnabled )
		return Plugin_Continue;

	new client = TE_ReadNum( "m_nPlayer" );

	// Prevent spray if they are banned.
	if ( g_bSprayBanned[client] ) {
		PrintToChat( client, "%sYou are banned from spraying decals.", MODTAG );
		return Plugin_Stop;
	}

	// Get location of new spray decal.
	new Float:vecOrigin[3];
	TE_ReadVector( "m_vecOrigin", vecOrigin );

	// Store location of new spray decal.
	g_flSprayLocs[client] = vecOrigin;

	g_bHasSprayed[client] = true;

	return Plugin_Continue;
}
















//-------------------------------------------
//
//
//			COMMANDS
//
//
//-------------------------------------------
















/********************************************
* Bans a player from being able to spray decals.
*/
public Action:cmdSprayBan( client, args )
{
	CHECK_ARGCOUNT( 2, "[SM] Usage: sm_sban <#userid|name> <duration in seconds> [reason]" )

	// Get a client id from the given userid or name
	BEGIN_PROCESS_TARGET()
	GetCmdArg( 1, sTargetString, sizeof(sTargetString) );
	PROCESS_TARGET( COMMAND_FILTER_CONNECTED | COMMAND_FILTER_NO_MULTI | COMMAND_FILTER_NO_BOTS )
	new target = aTargetList[0];

	// Declare variables to create baninfo with.
	decl String:sReason[MAX_REASON_LENGTH],
		 String:sArgDuration[16];
	new iDuration;

	// Calculate the unix timestamp of the future date when this ban expires.
	// iTime=0 means never expire.
	// GetCmdArg( 2, sTime, sizeof(sTime) );
	// iTime = StringToInt( sTime );
	// if (iTime == 0)
	// 	iExpires = 0;
	// else
	// 	iExpires = GetTime() + iTime;
	GetCmdArg( 2, sArgDuration, sizeof(sArgDuration) );
	if ( StringToIntEx( sArgDuration, iDuration ) < strlen( sArgDuration ) )
	{
		ReplyToCommand( client, "Invalid duration" );
		return Plugin_Handled;
	}

	// Concatenate the rest of the arguments into a single string to use as the reason.
	if ( args == 3 ) {
		GetCmdArg( 3, sReason, sizeof(sReason) );
	}
	else if ( args >= 3 ) {
		decl String:sArgBit[32];
		GetCmdArg( 3, sArgBit, sizeof(sArgBit) );
		Format( sReason, MAX_REASON_LENGTH, "%s%s", sArgBit, (args == 3) ? "" : " " );

		for ( new i=4; i<=args; i++ ) {
			GetCmdArg( i, sArgBit, sizeof(sArgBit) );
			Format( sReason, MAX_REASON_LENGTH, "%s%s%s", sReason, sArgBit, (args == i) ? "" : " " );	
		}
	}
	else {
		sReason = "None.";
	}

	// Ban the client from spraying. Store the baninfo record into the database.
	SPRAY_SprayBanClient( client, target, iDuration, sReason );

	ShowActivity2( client, MODTAG, "Banned '%N' from using sprays", target );
	LogAction( client, target, "banned %L from using sprays", target );

	return Plugin_Handled;
}

/********************************************
* Unbans a player so they can spray decals again.
*/
public Action:cmdSprayUnBan( client, args )
{
	CHECK_ARGCOUNT( 1, "[SM] Usage: sm_sunban <#userid|name>" )

	BEGIN_PROCESS_TARGET()
	GetCmdArg( 1, sTargetString, sizeof(sTargetString) );
	PROCESS_TARGET( COMMAND_FILTER_CONNECTED | COMMAND_FILTER_NO_MULTI | COMMAND_FILTER_NO_BOTS )

	new target = aTargetList[0];
	SPRAY_SprayUnbanClient( client, target );

	ShowActivity2( client, MODTAG, "Unbanned '%N' from using sprays.", target );
	LogAction( client, target, "unbanned %L from using sprays", target );

	return Plugin_Handled;
}



/********************************************
* Calls SPRAY_RemovePlayerDecal for targetted players.
*/
public Action:cmdRemoveSpray( client, args )
{
	CHECK_ARGCOUNT( 1, "[SM] Usage: sm_rspray <#userid|name>" )

	BEGIN_PROCESS_TARGET()
	GetCmdArg( 1, sTargetString, sizeof(sTargetString) );
	PROCESS_TARGET( COMMAND_FILTER_CONNECTED )

	for ( new i=0; i<iTargetCount; i++ ) {
		new target = aTargetList[i];
		SPRAY_RemovePlayerDecal( target );
		LogAction( client, target, "removed %N's (%i) spray from map.", target, target );
	}

	ShowActivity2( client, MODTAG, "Removed '%s' spray from the map.", sTargetName );
	return Plugin_Handled;
}



/********************************************
* Creates a spray where the client is aiming, even in spectator.
*/
public Action:cmdMakeSpray( client, args )
{
	decl target;
	decl Float:vecEndPos[3];

	if ( args > 0 ) {
		BEGIN_PROCESS_TARGET()
		GetCmdArg( 1, sTargetString, sizeof(sTargetString) );
		PROCESS_TARGET( COMMAND_FILTER_CONNECTED | COMMAND_FILTER_NO_BOTS | COMMAND_FILTER_NO_MULTI )

		target = aTargetList[0];
	} else {
		target = client;
	}

	if ( SPRAY_GetClientEyeTrace(client, vecEndPos) )
		SPRAY_CreatePlayerDecal(target, vecEndPos );

	return Plugin_Handled;
}


/********************************************
* Brings up the ban list menu.
*/
public Action:cmdShowBanList( client, args )
{
	// decl String:test[123];
	// // week:604800 day:86400 hour:3600 min:60
	// FormatBanExpires( GetTime() + (86400 * 3) + (3600 * 26), test, sizeof(test) );
	// ReplyToCommand( client, test );


	new Handle:testPanel = CreatePanel();

	SetPanelTitle( testPanel, "Test Panel Title" );
	DrawPanelText( testPanel, "Test Text\nNew Line\nBlah" );
	DrawPanelText( testPanel, "2nd Test" );
	DrawPanelItem( testPanel, "Yes" );
	DrawPanelItem( testPanel, "No" );
	SendPanelToClient( testPanel, client, testHandler, 9999 );
	CloseHandle( testPanel );

	return Plugin_Handled;
}
public testHandler( Handle:menu, MenuAction:action, param1, param2 )
{
	Debug_PrintToServer( "Test Panel Handler ---" );
	Debug_PrintToServer( "\taction: %i", _:action );
	Debug_PrintToServer( "\tparam1: %i", param1 );
	Debug_PrintToServer( "\tparam2: %i", param2 );
}


public Action:cmdUnlockDB( client, args )
{
	SQL_UnlockDatabase( g_hSQL );
	ReplyToCommand( client, "unlocked db" );
	return Plugin_Handled;
}
















//-------------------------------------------
//
//
//			DATABASE
//
//
//-------------------------------------------
















//*******************************************
// 		Initialisation
//*******************************************


SPRAY_SQLInitialise()
{
	LogMessage( "NOTICE:\t\tInitialising SQL subsystem" );
	if ( g_adtQueryQueue != INVALID_HANDLE ) {
		CloseHandle( g_adtQueryQueue );
	}
	g_adtQueryQueue = CreateArray( csqiSize, MAX_QUERY_QUEUE );
	g_iQueuePos = 0;

	if ( g_hSQL == INVALID_HANDLE ) {
		SPRAY_SQLConnect();
	}

	g_timerQueryQueueProc = CreateTimer( GetConVarFloat(cvSQLQueueProcRate) * 0.001, timerProcessQueryQueue, INVALID_HANDLE, TIMER_REPEAT );
	Debug_PrintToServer( "g_timerQueryQueueProc: %8X", g_timerQueryQueueProc );
}


SPRAY_SQLFinaliseInit()
{	
	if ( g_bLateLoad ) {
		decl String:sAuth[ MAX_AUTH_LENGTH ];

		LogMessage( "NOTICE:\tPlugin was late-loaded. Checking for bans." );
		for ( new i = 1; i <= MaxClients; i++ ) {
			if ( !Client_IsValid(i) )
				continue;
			GetClientAuthString( i, sAuth, MAX_AUTH_LENGTH );
			SPRAY_SQLEnqueueCheckOnConnect( i, sAuth );
		}
	}

	LogMessage( "NOTICE:\tSpraybans ready!" );
	g_bSQLReady = true;
}


SPRAY_SQLUninitialise()
{
	KillTimer( g_timerQueryQueueProc );

	LogMessage( "NOTICE:\tShutting down SQL" );
	SPRAY_SQLDisconnect();

	if ( g_adtQueryQueue != INVALID_HANDLE ) {
		CloseHandle( g_adtQueryQueue );
	}
}



/********************************************
* Connects to the SQL database.
*/
SPRAY_SQLConnect()
{
	if ( !SQL_CheckConfig( CONNECTION_CONFIG ) ) {
		LogError( "\tFATAL:\tCould not find connection info %s in databases.cfg", CONNECTION_CONFIG );
		SPRAY_SQLSetFailed( "could not find connection info %s in databases.cfg" );
		return;
	}
	g_iSQLNumConnectAttempts = 1;
	LogMessage( "NOTICE:\tAttempting database connection 1 of %i", GetConVarInt(cvSQLMaxAttempts) );
	SQL_TConnect( SQLConnectCallback, CONNECTION_CONFIG );
}



/********************************************
* Disconnects from the SQL database and resets all boolean values related to their "off" state.
*/
SPRAY_SQLDisconnect()
{
	LogMessage( "NOTICE:\tDisconnecting from database" );
	if ( g_hSQL != INVALID_HANDLE )
		CloseHandle( g_hSQL );

	g_hSQL = INVALID_HANDLE;
	g_bSQLReady = false;
}



/********************************************
* Called by sourcemod when threaded connection completes
*/
public SQLConnectCallback( Handle:driver, Handle:conn, const String:error[], any:data ) 
{
	decl String:sErrorMsg[256];
	sErrorMsg[0] = '\0';

	if ( conn == INVALID_HANDLE ) {
		if ( g_iSQLNumConnectAttempts < GetConVarInt(cvSQLMaxAttempts) ) {
			LogError( "\tWARN:\tFailed to connect to database. Retrying..." );
			CreateTimer( GetConVarFloat(cvSQLRetryDelay), timerSQLConnectRetry );
			return;
		}
		else {
			LogError( "\tFATAL:\tFailed to connect to database after %i retries", GetConVarInt(cvSQLMaxAttempts) );
			SPRAY_SQLSetFailed( "failed to establish connection to database, check settings and database logs" );
			return;
		}
	}

	LogMessage( "NOTICE:\tConnected to database successfully." );

	g_hSQL = CloneHandle(conn);

	SQL_SetCharset( g_hSQL, "utf8" );
	// SQL_LockDatabase( g_hSQL );
	// SQL_FastQuery( g_hSQL, "SET NAMES utf8;" );
	// SQL_UnlockDatabase( g_hSQL );

	if ( SPRAY_SQLCreateTable( sErrorMsg, sizeof(sErrorMsg) ) ) {
		SPRAY_SQLFinaliseInit();
	}
	else {
		LogMessage( "NOTICE:\tErrors occured during database initialisation. SQL is disabled this session." );
		SPRAY_SQLSetFailed( sErrorMsg );
	}
}
public Action:timerSQLConnectRetry( Handle:self )
{
	g_iSQLNumConnectAttempts++;
	LogMessage( "NOTICE:\tAttempting database connection %i of %i", g_iSQLNumConnectAttempts, GetConVarInt(cvSQLMaxAttempts) );
	SQL_TConnect( SQLConnectCallback, CONNECTION_CONFIG );
}


#define TABLE_NOT_EXISTS_VERSION 0x1
#define TABLE_NOT_EXISTS_BAN 0x2

/********************************************
* Checks that the tables exist in the current database.
* DO NOT CALL WITHOUT LOCKING DATABASE FIRST!!!
*/
SPRAY_SQLCheckTables( &retval )
{
	new Handle: hQuery;

	// Check if the version table exists.
	//hQuery = SQL_Query( g_hSQL, "SELECT `INFORMATION_SCHEMA`.`TABLES`.`TABLE_NAME` FROM `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA` = \"sprayban\" AND `TABLE_NAME` = \"smc_sprayban_dbver\";" );
	hQuery = SQL_Query( g_hSQL, "SHOW TABLES LIKE \"smc_sprayban_dbver\";" );
	Debug_PrintToServer( "show tables like smc_sprayban_dbver: query=%08X, rowcount=%i", hQuery, SQL_GetRowCount(hQuery) );
	if ( hQuery == INVALID_HANDLE || SQL_GetRowCount(hQuery) != 1 )
		retval |= TABLE_NOT_EXISTS_VERSION;
	CloseHandle(hQuery);

	// Check if the ban table exists.
	//hQuery = SQL_Query( g_hSQL, "SELECT `INFORMATION_SCHEMA`.`TABLES`.`TABLE_NAME` FROM `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA` = \"sprayban\" AND `TABLE_NAME` = \"smc_sprayban\";" );
	hQuery = SQL_Query( g_hSQL, "SHOW TABLES LIKE \"smc_sprayban\";" );
	Debug_PrintToServer( "show tables like smc_sprayban: query=%08X, rowcount=%i", hQuery, SQL_GetRowCount(hQuery) );
	if ( hQuery == INVALID_HANDLE || SQL_GetRowCount(hQuery) != 1 )
		retval |= TABLE_NOT_EXISTS_BAN;
	CloseHandle(hQuery);
}


/********************************************
* Creates a new table in the database if it does not already exist.
*/
bool:SPRAY_SQLCreateTable( String:errorBuffer[], err_len )
{
	new         tableCheckValue;
	new Handle: hQuery;
	new String: sQueryString[512],
		String: sErrorString[256];

	SQL_LockDatabase( g_hSQL );
	SPRAY_SQLCheckTables( tableCheckValue );

	// Create the version table if it does not exist.
	if ( tableCheckValue & TABLE_NOT_EXISTS_VERSION ) {
		LogMessage( "NOTICE:\tCreating new table smc_sprayban_dbver" );
		Format( sQueryString, sizeof(sQueryString), 
			"CREATE TABLE IF NOT EXISTS `smc_sprayban_dbver` ( `KEY` VARCHAR(32) NOT NULL, `VALUE` VARCHAR(128) DEFAULT '1', PRIMARY KEY (`KEY`)) ENGINE = InnoDB CHARACTER SET UTF8 COLLATE utf8_general_ci;" );
		hQuery = SQL_Query( g_hSQL, sQueryString );

		if ( hQuery == INVALID_HANDLE ) {
			SQL_GetError( g_hSQL, sErrorString, sizeof(sErrorString) );
			LogError( "FATAL:\tError in creating new table smc_sprayban_dbver" );
			LogError( "FATAL:\tDatabase reported: %s", sErrorString );
			Format( errorBuffer, err_len, "error creating table. %s", sErrorString );
			SQL_UnlockDatabase(g_hSQL);
			return false;
		}
		CloseHandle(hQuery);

		Format( sQueryString, sizeof(sQueryString), 
			"INSERT INTO `smc_sprayban_dbver` (`KEY`,`VALUE`) VALUES ('VERSION','%i') ON DUPLICATE KEY UPDATE `VALUE`='%i';", SQL_DATABASE_VERSION, SQL_DATABASE_VERSION );
		hQuery = SQL_Query( g_hSQL, sQueryString );

		if ( hQuery == INVALID_HANDLE ) {
			SQL_GetError( g_hSQL, sErrorString, sizeof(sErrorString) );
			LogError( "FATAL:\tError in creating new table smc_sprayban" );
			LogError( "FATAL:\tDatabase reported: %s", sErrorString );
			Format( errorBuffer, err_len, "error creating table. %s", sErrorString );
			SQL_UnlockDatabase(g_hSQL);
			return false;
		}
		CloseHandle(hQuery);
	}
	else {
		LogMessage( "NOTICE:\tTable smc_sprayban_dbver exists" );
	}

	// Create ban table if it does not exist.
	if ( tableCheckValue & TABLE_NOT_EXISTS_BAN ) {
		LogMessage( "NOTICE:\tCreating new table smc_sprayban" );
		Format( sQueryString, sizeof(sQueryString), "CREATE TABLE IF NOT EXISTS `smc_sprayban` ( \
			`auth` VARCHAR(32) NOT NULL, \
			`name` VARCHAR(32) DEFAULT '<unknown>', \
			`reason` VARCHAR(128) DEFAULT 'None', \
			`expires` INT UNSIGNED DEFAULT 0, \
			`adminauth` VARCHAR(32) DEFAULT 'Console', \
			`adminname` VARCHAR(32) DEFAULT 'Console', \
			PRIMARY KEY (`auth`)) \
			ENGINE = InnoDB \
			CHARACTER SET utf8 \
			COLLATE utf8_general_ci;" );

		hQuery = SQL_Query( g_hSQL, sQueryString );

		if ( hQuery == INVALID_HANDLE ) {
			SQL_GetError( g_hSQL, sErrorString, sizeof(sErrorString) );
			LogError( "FATAL:\tError in creating new table smc_sprayban" );
			LogError( "FATAL:\tDatabase reported: %s", sErrorString );
			Format( errorBuffer, err_len, "error creating table. %s", sErrorString );
			SQL_UnlockDatabase(g_hSQL);
			return false;
		}
		CloseHandle(hQuery);
	}
	else {
		LogMessage( "NOTICE:\tTable smc_sprayban exists" );
	}

	SQL_UnlockDatabase( g_hSQL );
	return true;
}















//*******************************************
// 		Query Queue
//*******************************************

#define QUERY_REMOVEFROMQUEUE(%1) RemoveFromArray( g_adtQueryQueue, %1 );\
	g_iQueuePos--

/********************************************
* 
*/
public Action:timerProcessQueryQueue( Handle:self )
{
	if ( !g_bSQLEnabled )
		return Plugin_Handled;

	if ( g_iQueuePos <= 0 )
		return Plugin_Handled;

	LogMessage( "NOTICE:\tQueue processing. g_iQueuePos: %i", g_iQueuePos );

	new CSprayQueryInfo:qi[ csqiSize ];
	new ptr;

	for ( ptr = 0; ptr < g_iQueuePos; ptr++ )
	{
		GetArrayArray( g_adtQueryQueue, ptr, qi, csqiSize );
		if ( !qi[m_bRunning] )
			break;
	}

	// Final check. TODO: Replace this for loop with something more robust so it doesn't have to check the last query twice.
	if ( qi[m_bRunning] )
		return Plugin_Handled;

	qi[m_bRunning] = true;
	qi[m_nAttemptCount] += 1;

	SetArrayArray( g_adtQueryQueue, ptr, qi, csqiSize );

	switch ( qi[m_qtype] )
	{
		case QueryType_CheckExistsOnConnect, QueryType_CheckExistsForStorage, QueryType_CheckExistsForRemoval: 
		{
			LogMessage( "NOTICE:\tType: QueryType_CheckExistsOnConnect" );
			decl String:sQueryString[ 1024 ],
				 String:sAuthEscaped[ MAX_AUTH_ESCAPED ];

			ResetPack( qi[m_hQueryData] );
			ReadPackCell( qi[m_hQueryData] ); // skip callingClient
			ReadPackCell( qi[m_hQueryData] ); // skip targetClient
			ReadPackString( qi[m_hQueryData], sAuthEscaped, MAX_AUTH_ESCAPED );

			Format( sQueryString, sizeof(sQueryString), "SELECT * FROM `smc_sprayban` WHERE `auth` = \"%s\";", sAuthEscaped );

			if ( qi[m_qtype] == QueryType_CheckExistsOnConnect )
				SQL_TQuery( g_hSQL, callbackCheckAuthQueryOnConnect, sQueryString, qi[m_nQueryUID] );
			else if ( qi[m_qtype] == QueryType_CheckExistsForStorage )
				SQL_TQuery( g_hSQL, callbackCheckAuthQueryForStoring, sQueryString, qi[m_nQueryUID] );
			else if ( qi[m_qtype] == QueryType_CheckExistsForRemoval )
				SQL_TQuery( g_hSQL, callbackCheckAuthQueryForRemoval, sQueryString, qi[m_nQueryUID] );
		}

		case QueryType_Store:
		{
			LogMessage( "NOTICE:\tType: QueryType_Store" );

			decl String:sQueryString[ 1024 ],
					String:sAuthEscaped[ MAX_AUTH_ESCAPED ],
					String:sNameEscaped[ MAX_NAME_ESCAPED ],
					String:sAdminAuthEscaped[ MAX_AUTH_ESCAPED ],
					String:sAdminNameEscaped[ MAX_NAME_ESCAPED ],
					String:sReasonEscaped[ MAX_REASON_ESCAPED ];
			new iExpires;

			ResetPack( qi[m_hQueryData] );
			ReadPackCell( qi[m_hQueryData] ); // skip callingClient
			ReadPackCell( qi[m_hQueryData] ); // skip targetClient
			ReadPackString( qi[m_hQueryData], sAuthEscaped, MAX_AUTH_ESCAPED );
			ReadPackString( qi[m_hQueryData], sNameEscaped, MAX_NAME_ESCAPED );
			ReadPackString( qi[m_hQueryData], sAdminAuthEscaped, MAX_AUTH_ESCAPED );
			ReadPackString( qi[m_hQueryData], sAdminNameEscaped, MAX_NAME_ESCAPED );
			ReadPackString( qi[m_hQueryData], sReasonEscaped, MAX_REASON_ESCAPED );
			iExpires = ReadPackCell( qi[m_hQueryData] );

			Format( sQueryString, sizeof(sQueryString), "REPLACE INTO `smc_sprayban` SET `auth` = \"%s\", `name` = \"%s\", `reason` = \"%s\", `expires` = %i, `adminauth` = \"%s\", `adminname` = \"%s\";", 
						sAuthEscaped, 
						sNameEscaped,
						sReasonEscaped,
						iExpires,
						sAdminAuthEscaped,
						sAdminNameEscaped );

			SQL_TQuery( g_hSQL, callbackStoreBanQuery, sQueryString, qi[m_nQueryUID] );
		}

		case QueryType_Remove:
		{
			LogMessage( "NOTICE:\tType: QueryType_Remove" );

			decl String:sQueryString[ 1024 ],
					String:sAuthEscaped[ MAX_AUTH_ESCAPED ];

			ResetPack( qi[m_hQueryData] );
			ReadPackCell( qi[m_hQueryData] ); // skip callingClient
			ReadPackCell( qi[m_hQueryData] ); // skip targetClient
			ReadPackString( qi[m_hQueryData], sAuthEscaped, MAX_AUTH_ESCAPED );

			Format( sQueryString, sizeof(sQueryString), "DELETE FROM `smc_sprayban` WHERE `auth` = \"%s\";", sAuthEscaped );

			SQL_TQuery( g_hSQL, callbackRemoveBanQuery, sQueryString, qi[m_nQueryUID] );
			QUERY_REMOVEFROMQUEUE(ptr);
		}

		case QueryType_Get:
		{
			LogMessage( "NOTICE:\tType: QueryType_Get" );

			decl String:sQueryString[ 1024 ],
					String:sAuthEscaped[ MAX_AUTH_ESCAPED ];

			ResetPack( qi[m_hQueryData] );
			ReadPackCell( qi[m_hQueryData] ); // skip callingClient
			ReadPackString( qi[m_hQueryData], sAuthEscaped, MAX_AUTH_ESCAPED );

			Format( sQueryString, sizeof(sQueryString), "SELECT * FROM `smc_sprayban` WHERE `auth`=\"%s\";", sAuthEscaped );

			// SQL_TQuery( g_hSQL, callbackGetBanQuery, sQueryString, qi[m_nQueryUID] );
			QUERY_REMOVEFROMQUEUE(ptr);
		}

		case QueryType_GetMass:
		{
			LogMessage( "NOTICE:\tType: QueryType_GetMass" );

			decl String:sQueryString[ 1024 ];
			new offset, amount;

			ResetPack( qi[m_hQueryData] );
			ReadPackCell( qi[m_hQueryData] );
			offset = ReadPackCell( qi[m_hQueryData] );
			amount = ReadPackCell( qi[m_hQueryData] );

			Format( sQueryString, sizeof(sQueryString), "SELECT * FROM `smc_sprayban` ORDER BY `name` ASC LIMIT %i,%i;", offset, amount );

			// SQL_TQuery( g_hSQL, callbackGetBanMassQuery, sQueryString, qi[m_nQueryUID] );
			QUERY_REMOVEFROMQUEUE(ptr);
		}
	}

	return Plugin_Handled;
}




/********************************************
* Sets up a new QueryInfo for insertion into the queue.
*
* @param 	qi 			QueryInfo to setup.
* @param 	queryData 	Datapack that contains variables necessary for query.
* @param 	querytype 	The type of query so the plugin knows what statement to use.
* @noreturn
*/
SPRAY_SQLSetupQueryInfo( CSprayQueryInfo:qi[ csqiSize ], Handle:queryData, QueryType:querytype )
{
	// Generate query UID.
	qi[m_nQueryUID] = GetTime();

	// Init the queryInfo to default values.
	qi[m_qtype] 			= querytype;
	qi[m_bRunning] 		= false;
	qi[m_nAttemptCount] 	= 0;
	qi[m_hQueryData] 		= queryData;
}

SPRAY_SQLEscapeBanInfo( CSprayBanInfo:bi[ csbiSize ] )
{
	if ( g_hSQL == INVALID_HANDLE )
		return;

	decl 	String:sAuthEscaped[ MAX_AUTH_ESCAPED ],
			String:sNameEscaped[ MAX_NAME_ESCAPED ],
			String:sAdminAuthEscaped[ MAX_AUTH_ESCAPED ],
			String:sAdminNameEscaped[ MAX_NAME_ESCAPED ],
			String:sReasonEscaped[ MAX_REASON_ESCAPED ];

	SQL_EscapeString( g_hSQL, bi[auth], sAuthEscaped, MAX_AUTH_ESCAPED );
	SQL_EscapeString( g_hSQL, bi[name], sNameEscaped, MAX_NAME_ESCAPED );
	SQL_EscapeString( g_hSQL, bi[adminauth], sAdminAuthEscaped, MAX_AUTH_ESCAPED );
	SQL_EscapeString( g_hSQL, bi[adminname], sAdminNameEscaped, MAX_NAME_ESCAPED );
	SQL_EscapeString( g_hSQL, bi[reason], sReasonEscaped, MAX_NAME_ESCAPED );

	strcopy( bi[auth], MAX_AUTH_ESCAPED, sAuthEscaped );
	strcopy( bi[name], MAX_NAME_ESCAPED, sNameEscaped );
	strcopy( bi[adminauth], MAX_AUTH_ESCAPED, sAdminAuthEscaped );
	strcopy( bi[adminname], MAX_NAME_ESCAPED, sAdminNameEscaped );
	strcopy( bi[reason], MAX_REASON_ESCAPED, sReasonEscaped );
}



/********************************************
* Queues query to check the database if the given auth string exists.
*
* @param 	sAuth 		Auth string to check for.
* @callback 			callbackCheckAuthQueryOnConnect
*/
SPRAY_SQLEnqueueCheckOnConnect( connectingClient, const String:sAuth[ MAX_AUTH_LENGTH ] )
{
	if ( !g_bSQLEnabled || g_hSQL == INVALID_HANDLE )
		return;

	decl String:sAuthEscaped[ MAX_AUTH_ESCAPED ];
	SQL_EscapeString( g_hSQL, sAuth, sAuthEscaped, MAX_AUTH_ESCAPED );

	decl CSprayQueryInfo:qi[ csqiSize ];
	new Handle:queryData = CreateDataPack();
	WritePackCell( queryData, connectingClient );
	WritePackString( queryData, sAuth );
	ResetPack(queryData);

	SPRAY_SQLSetupQueryInfo( qi, queryData, QueryType_CheckExistsOnConnect );

	SetArrayArray( g_adtQueryQueue, g_iQueuePos, qi, csqiSize );
	g_iQueuePos++;
	LogMessage( "NOTICE:\tAdded CheckOnConnectQuery to queue" );
}


/********************************************
* Queues query to check the database if the auth string contained in the supplied 
* baninfo already exists in the database.
*
* @param 	callingClient 	The client index that prompted this query
* @param	auth 			Client auth string to check for
* @param 	checkMode 		The purpose we are checking for, storing or deleting
* @callback 				callbackCheckAuthQueryForStoring
* @callback 				callbackCheckAuthQueryForRemoval
*/
SPRAY_SQLEnqueueCheckBanQuery( callingClient, targetClient, CSprayBanInfo:baninfo[ csbiSize ], checkMode )
{
	if ( !g_bSQLEnabled || g_hSQL == INVALID_HANDLE )
		return;

	LogMessage( "\t\tNOTICE:\tSPRAY_SQLEnqueueCheckBanQuery CALLED" );

	decl CSprayQueryInfo:qi[ csqiSize ];

	SPRAY_SQLEscapeBanInfo( baninfo );

	new Handle:queryData = CreateDataPack();
	WritePackCell( queryData, callingClient );
	WritePackCell( queryData, targetClient );
	WritePackString( queryData, baninfo[auth] );
	WritePackString( queryData, baninfo[name] );
	WritePackString( queryData, baninfo[adminauth] );
	WritePackString( queryData, baninfo[adminname] );
	WritePackString( queryData, baninfo[reason] );
	WritePackCell( queryData, baninfo[expires] );
	ResetPack(queryData);

	switch ( checkMode )
	{
		case CHECKQUERY_STORE:
		{
			SPRAY_SQLSetupQueryInfo( qi, queryData, QueryType_CheckExistsForStorage );			
		}
		case CHECKQUERY_REMOVE:
		{
			SPRAY_SQLSetupQueryInfo( qi, queryData, QueryType_CheckExistsForRemoval );
		}
	}

	new index = SetArrayArray( g_adtQueryQueue, g_iQueuePos, qi, csqiSize );
	g_iQueuePos++;
	LogMessage( "NOTICE:\tAdded CheckBanQuery to queue. Index: %i, g_iQueuePos: %i", index, g_iQueuePos );
}



/********************************************
* Inserts a new ban record into the database.
*
* @param 	callingClient 	The client index that prompted this query
* @param 	baninfo 		A CSprayBanInfo object containing the ban record for storing.
* @callback 			callbackStoreBanQuery
*/
SPRAY_SQLEnqueueStoreBanQuery( callingClient, targetClient, CSprayBanInfo:baninfo[ csbiSize ] )
{
	if ( !g_bSQLEnabled || g_hSQL == INVALID_HANDLE )
		return;

	decl CSprayQueryInfo:qi[ csqiSize ];

	SPRAY_SQLEscapeBanInfo( baninfo );

	new Handle:queryData = CreateDataPack();
	WritePackCell( queryData, callingClient );
	WritePackCell( queryData, targetClient );
	WritePackString( queryData, baninfo[auth] );
	WritePackString( queryData, baninfo[name] );
	WritePackString( queryData, baninfo[adminauth] );
	WritePackString( queryData, baninfo[adminname] );
	WritePackString( queryData, baninfo[reason] );
	WritePackCell( queryData, baninfo[expires] );
	ResetPack(queryData);

	SPRAY_SQLSetupQueryInfo( qi, queryData, QueryType_Store );

	new index = SetArrayArray( g_adtQueryQueue, g_iQueuePos, qi, csqiSize );
	g_iQueuePos++;
	LogMessage( "NOTICE:\tAdded StoreBanQuery to queue. Index: %i, g_iQueuePos: %i", index, g_iQueuePos );
}


SPRAY_SQLEnqueueStoreBanByQueryInfo( CSprayQueryInfo:qi[ csqiSize ] )
{
	if ( !g_bSQLEnabled || g_hSQL == INVALID_HANDLE )
		return;

	decl CSprayQueryInfo:newqi[ csqiSize ];
	
	SPRAY_SQLSetupQueryInfo( newqi, qi[m_hQueryData], QueryType_Store );

	SetArrayArray( g_adtQueryQueue, g_iQueuePos, newqi, csqiSize );
	g_iQueuePos++;
	LogMessage( "NOTICE:\tAdded SPRAY_SQLEnqueueStoreBanByQueryInfo to queue." );
}


/********************************************
* Removes a ban record from the database.
*
* @param 	callingClient 	The client index that prompted this query
* @param 	auth 			The client auth string to remove.
* @callback 				callbackRemoveBanQuery
*/
SPRAY_SQLEnqueueRemoveBanRecordQuery( callingClient, targetClient, const String:sAuth[MAX_AUTH_LENGTH] )
{
	if ( !g_bSQLEnabled || g_hSQL == INVALID_HANDLE )
		return;

	decl String:sAuthEscaped[ MAX_AUTH_ESCAPED ],
		 String:sNameEscaped[ MAX_NAME_ESCAPED ];
	decl CSprayQueryInfo:qi[ csqiSize ];

	SQL_EscapeString( g_hSQL, sAuth, sAuthEscaped, MAX_AUTH_ESCAPED );

	new Handle:queryData = CreateDataPack();
	WritePackCell( queryData, callingClient );
	WritePackCell( queryData, targetClient );
	WritePackString( queryData, sAuthEscaped );
	WritePackString( queryData, sNameEscaped );
	ResetPack(queryData);

	SPRAY_SQLSetupQueryInfo( qi, queryData, QueryType_Remove );

	new index = SetArrayArray( g_adtQueryQueue, g_iQueuePos, qi, csqiSize );
	g_iQueuePos++;
	LogMessage( "NOTICE:\tAdded RemoveBanQuery to queue. Index: %i, g_iQueuePos: %i", index, g_iQueuePos );
}


SPRAY_SQLEnqueueRemoveBanRecordByQueryInfo( CSprayQueryInfo:qi[ csqiSize ] )
{
	if ( !g_bSQLEnabled || g_hSQL == INVALID_HANDLE )
		return;

	decl CSprayQueryInfo:newQI[ csqiSize ];
	SPRAY_SQLSetupQueryInfo( newQI, qi[m_hQueryData], QueryType_Remove );

	SetArrayArray( g_adtQueryQueue, g_iQueuePos, newQI, csqiSize );
	g_iQueuePos++;
	LogMessage( "NOTICE:\tAdded SPRAY_SQLEnqueueRemoveBanRecordByQueryInfo to queue." );
}



/********************************************
* Retrieves a group of ban records from the database.
*
* @param 	callingClient 	The client index that prompted this query
* @param 	auth 			The client auth string to retrieve.
* @param 	dataStore		The baninfo object to store the retrieved ban data.
* @callback 				callbackGetBanQuery
*/
SPRAY_SQLEnqueueGetBanQuery( callingClient, const String:sAuth[MAX_AUTH_LENGTH] )
{
	if ( !g_bSQLEnabled || g_hSQL == INVALID_HANDLE )
		return;

	decl String:sAuthEscaped[ MAX_AUTH_ESCAPED ];
	decl CSprayQueryInfo:qi[ csqiSize ];

	SQL_EscapeString( g_hSQL, sAuth, sAuthEscaped, MAX_AUTH_ESCAPED );

	new Handle:queryData = CreateDataPack();
	WritePackCell( queryData, callingClient );
	WritePackString( queryData, sAuth );
	ResetPack(queryData);

	SPRAY_SQLSetupQueryInfo( qi, queryData, QueryType_Get );

	new index = SetArrayArray( g_adtQueryQueue, g_iQueuePos, qi, csqiSize );
	g_iQueuePos++;
	LogMessage( "NOTICE:\tAdded GetBanQuery to queue. Index: %i, g_iQueuePos: %i", index, g_iQueuePos );
}



/********************************************
* Retrieves a group of ban records from the database.
*
* @param 	callingClient 	The client index that prompted this query
* @param 	offset 			The row position to begin reading from the database
* @param 	number 			Amount of rows to read.
* @callback 				callbackGetBanMassQuery
*/
SPRAY_SQLEnqueueGetBanMassQuery( callingClient, offset, number )
{
	if ( !g_bSQLEnabled || g_hSQL == INVALID_HANDLE )
		return;

	decl CSprayQueryInfo:qi[ csqiSize ];

	new Handle:queryData = CreateDataPack();
	WritePackCell( queryData, callingClient );
	WritePackCell( queryData, offset );
	WritePackCell( queryData, number );
	ResetPack(queryData);

	SPRAY_SQLSetupQueryInfo( qi, queryData, QueryType_GetMass );

	new index = SetArrayArray( g_adtQueryQueue, g_iQueuePos, qi, csqiSize );
	g_iQueuePos++;
	LogMessage( "NOTICE:\tAdded GetBanMassQuery to queue. Index: %i, g_iQueuePos: %i", index, g_iQueuePos );
}



/********************************************
* Retrieves a group of ban records from the database.
*/
SPRAY_SQLReplyToSQLError( client, errCode )
{
}


/********************************************
* Retrieves a group of ban records from the database.
*/
SPRAY_SQLSetFailed( const String:sFmt[], ... )
{
	decl String:msg[256];
	VFormat( msg, sizeof(msg), sFmt, 2 );
	SetConVarString( cvSQLLastError, msg );
	SetConVarBool( cvSQLEnabled, false );
}













SPRAY_SQLGetQueryInfoByUID( queryUID, CSprayQueryInfo:qi[ csqiSize ] )
{
	for ( new i=0; i < g_iQueuePos; i++ ) {
		GetArrayArray( g_adtQueryQueue, i, qi, csqiSize );
		if ( qi[m_nQueryUID] == queryUID )
			return i;
	}
	return -1;
}

//*******************************************
// 		Query Callbacks
//*******************************************

public callbackCheckAuthQueryOnConnect( Handle:db, Handle:q, const String:error[], any:queryUID )
{
	decl CSprayQueryInfo:qi[ csqiSize ];
	new queryIndex,
		connectingClient;

	if ( (queryIndex = SPRAY_SQLGetQueryInfoByUID( queryUID, qi )) < 0 )
		return;

	if ( q == INVALID_HANDLE ) {
		LogError( "WARN:\tCheckOnConnect query failed: \"%s\"", error );

		SPRAY_SQLDisconnect();

		if ( qi[m_nAttemptCount] >= GetConVarInt(cvSQLQueryMaxAtmps) ) {
			LogError( "FATAL:\tCheckOnConnect query failed after %i attempts! Removing!", qi[m_nAttemptCount] );
			QUERY_REMOVEFROMQUEUE(queryIndex);
		}
		else {
			LogError( "WARN:\tRetrying CheckOnConnect query next queue proc" );
			qi[m_bRunning] = false;
			SetArrayArray( g_adtQueryQueue, queryIndex, qi, csqiSize );
		}		
		SPRAY_SQLConnect();
		return;
	}

	ResetPack( qi[m_hQueryData] );
	connectingClient = ReadPackCell( qi[m_hQueryData] );

	if ( Client_IsValid(connectingClient) ) {
		// Check results and shit.
		if ( SQL_GetRowCount(q) > 0 ) {
			LogMessage( "NOTICE:\tClient %N is SPRAYBANNED", connectingClient );
			g_bSprayBanned[connectingClient] = true;
		}
		else {
			LogMessage( "NOTICE:\tClient %N is NOT SPRAYBANNED", connectingClient );
			g_bSprayBanned[connectingClient] = false;
		}
	}

	QUERY_REMOVEFROMQUEUE(queryIndex);
}



public callbackCheckAuthQueryForStoring( Handle:db, Handle:q, const String:error[], any:queryUID )
{
	LogMessage( "NOTICE:\tcallbackCheckAuthQueryForStoring CALLED" );

	decl CSprayQueryInfo:qi[ csqiSize ];
	new queryIndex,
		callingClient,
		targetClient;

	if ( (queryIndex = SPRAY_SQLGetQueryInfoByUID( queryUID, qi )) < 0 )
		return;

	if ( q == INVALID_HANDLE ) {
		LogError( "WARN:\tCheckAuthForStorage query failed: \"%s\"", error );

		SPRAY_SQLDisconnect();

		if ( qi[m_nAttemptCount] >= GetConVarInt(cvSQLQueryMaxAtmps) ) {
			QUERY_REMOVEFROMQUEUE(queryIndex);
		}
		else {
			qi[m_bRunning] = false;
			SetArrayArray( g_adtQueryQueue, queryIndex, qi, csqiSize );
		}
		SPRAY_SQLConnect();
		return;
	}

	ResetPack( qi[m_hQueryData] );
	callingClient = ReadPackCell( qi[m_hQueryData] );
	targetClient = ReadPackCell( qi[m_hQueryData] );

	if ( !Client_IsValid(callingClient) ) {
		QUERY_REMOVEFROMQUEUE( queryIndex );
	}

	if ( SQL_GetRowCount(q) > 0 ) {
		// Get the existing ban details.
		decl String:sName[MAX_NAME_LENGTH],
			 String:sAdminName[MAX_NAME_LENGTH],
			 String:sReason[MAX_REASON_LENGTH],
			 String:sExpires[256];
		new iExpires;

		SQL_FetchRow(q);
		SQL_FetchString( q, 1, sName, MAX_NAME_LENGTH );
		SQL_FetchString( q, 5, sAdminName, MAX_NAME_LENGTH );
		SQL_FetchString( q, 2, sReason, MAX_REASON_LENGTH );
		iExpires = SQL_FetchInt( q, 3 );

		FormatBanExpires( iExpires, sExpires, sizeof(sExpires) );

		// Check if client already has a menu displayed.
		if ( g_clientsPendingMenu[callingClient][m_bIsPending] ) {
			CancelClientMenu( callingClient );
			g_clientsPendingMenu[callingClient][m_bIsPending] = false;
			QUERY_REMOVEFROMQUEUE( g_clientsPendingMenu[callingClient][m_nQueryIndex] );
		}

		// Show confirmation menu.
		new Handle:confirmPanel = CreatePanel();

		decl String:sMsg[500];

		if ( Client_IsValid(targetClient) ) {
			Format( sMsg, sizeof(sMsg), "%N is already banned!\
				\nDo you want to overwrite?\
				\nOld Name:\t%s\
				\nAdmin:\t%s\
				\nReason:\t%s\
				\nExpires:\t%s", targetClient, sName, sAdminName, sReason, sExpires );
		}
		else {
			Format( sMsg, sizeof(sMsg), "%s is already banned!\
				\nDo you want to overwrite?\
				\nOld Name:\t%s\
				\nAdmin:\t%s\
				\nReason:\t%s\
				\nExpires:\t%s", sName, sName, sAdminName, sReason, sExpires );
		}

		SetPanelTitle( confirmPanel, "Confirm ban?" );
		DrawPanelText( confirmPanel, sMsg );
		DrawPanelItem( confirmPanel, "Yes" );
		DrawPanelItem( confirmPanel, "No" );
		SendPanelToClient( confirmPanel, callingClient, panelStoreBanConfirm, 300 );
		g_clientsPendingMenu[callingClient][m_bIsPending] = true;
		g_clientsPendingMenu[callingClient][m_nQueryIndex] = queryIndex;
		CloseHandle( confirmPanel );

		return;
	}

	SPRAY_SQLEnqueueStoreBanByQueryInfo( qi );
	QUERY_REMOVEFROMQUEUE(queryIndex);
}

public panelStoreBanConfirm( Handle:invalid, MenuAction:action, client, button )
{
	LogMessage( "\t\tpanelStoreBanConfirm:: action:%i client:%i item:%i", _:action, client, button );
	decl CSprayQueryInfo:qi[ csqiSize ];
	GetArrayArray( g_adtQueryQueue, g_clientsPendingMenu[client][m_nQueryIndex], qi, csqiSize );

	if ( action == MenuAction_Select && button == 1 ) {
		SPRAY_SQLEnqueueStoreBanByQueryInfo( qi );
	}
	
	QUERY_REMOVEFROMQUEUE( g_clientsPendingMenu[client][m_nQueryIndex] );
	g_clientsPendingMenu[client][m_bIsPending] = false;
}



public callbackCheckAuthQueryForRemoval( Handle:db, Handle:q, const String:error[], any:queryUID )
{
	LogMessage( "\t\tNOTICE:\tcallbackCheckAuthQueryForRemoval CALLED" );

	decl CSprayQueryInfo:qi[ csqiSize ];
	new  queryIndex;

	if ( (queryIndex = SPRAY_SQLGetQueryInfoByUID( queryUID, qi )) < 0 )
		return;

	if ( q == INVALID_HANDLE ) {
		LogError( "WARN:\tCheckAuthForRemoval query failed! Error: %s", error );

		SPRAY_SQLDisconnect();
		if ( qi[m_nAttemptCount] > GetConVarInt(cvSQLQueryMaxAtmps) ) {
			LogError( "FATAL:\tCheckAuthForRemoval query failed after %i attempts! Removing", qi[m_nAttemptCount] );
			QUERY_REMOVEFROMQUEUE(queryIndex);
		}
		else {
			qi[m_bRunning] = false;
			SetArrayArray( g_adtQueryQueue, queryIndex, qi, csqiSize );
		}
		SPRAY_SQLConnect();
		return;
	}

	if ( SQL_GetRowCount(q) > 0 ) {
		SPRAY_SQLEnqueueRemoveBanRecordByQueryInfo( qi );
	}

	QUERY_REMOVEFROMQUEUE(queryIndex);
}



public callbackStoreBanQuery( Handle:db, Handle:q, const String:error[], any:queryUID )
{
	decl CSprayQueryInfo:qi[ csqiSize ];
	new queryIndex,
		callingClient,
		targetClient;

	decl String:sAuth[MAX_AUTH_ESCAPED];

	if ( (queryIndex = SPRAY_SQLGetQueryInfoByUID( queryUID, qi)) < 0 ) {
		return;
	}	

	ResetPack( qi[m_hQueryData] );
	callingClient = ReadPackCell( qi[m_hQueryData] );
	targetClient  = ReadPackCell( qi[m_hQueryData] );
	ReadPackString( qi[m_hQueryData], sAuth, MAX_AUTH_ESCAPED );

	if ( q == INVALID_HANDLE ) {
		LogError( "WARN:\tStoreBan query failed: \"%s\"", error );

		SPRAY_SQLDisconnect();

		if ( qi[m_nAttemptCount] >= GetConVarInt(cvSQLQueryMaxAtmps) ) {
			PrintToChat( callingClient, "[SprayBan] Failed storing ban to database." );
			PrintToChat( callingClient, "[SprayBan] error: %s", error );
			QUERY_REMOVEFROMQUEUE(queryIndex);
		}
		else {
			qi[m_bRunning] = false;
			SetArrayArray( g_adtQueryQueue, queryIndex, qi, csqiSize );
		}
		SPRAY_SQLConnect();
		return;
	}

	// PrintToChat( callingClient, "[SprayBan] Banned %N from using sprays", targetClient );
	// g_bSprayBanned[targetClient] = true;
	LogMessage( "NOTICE:\tSprayban for %N <%s> stored to database", targetClient, sAuth );

	QUERY_REMOVEFROMQUEUE(queryIndex);
}



public callbackRemoveBanQuery( Handle:db, Handle:q, const String:error[], any:queryUID )
{
	decl CSprayQueryInfo:qi[ csqiSize ];
	new queryIndex,
		callingClient,
		targetClient;

	decl String:sAuth[MAX_AUTH_ESCAPED],
		 String:sName[MAX_NAME_ESCAPED];

	if ( (queryIndex = SPRAY_SQLGetQueryInfoByUID( queryUID, qi)) < 0 ) {
		return;
	}

	ResetPack( qi[m_hQueryData] );
	callingClient = ReadPackCell( qi[m_hQueryData] );
	ReadPackString( qi[m_hQueryData], sAuth, MAX_AUTH_ESCAPED );
	ReadPackString( qi[m_hQueryData], sName, MAX_NAME_ESCAPED );

	if ( q == INVALID_HANDLE ) {
		LogError( "WARN:\tRemoveBan query failed: \"%s\"", error );

		SPRAY_SQLDisconnect();
		if ( qi[m_nAttemptCount] >= GetConVarInt(cvSQLQueryMaxAtmps) ) {
			if ( Client_IsIngame(callingClient) ) {
				PrintToChat( callingClient, "[SprayMan] Unable to save unban to database." );
				PrintToChat( callingClient, "[SprayMan] Error: %s", error );
			}
			LogError( "FATAL:\tRemoveBan query failed after %i attempts. Removing", qi[m_nAttemptCount] );
			QUERY_REMOVEFROMQUEUE(queryIndex);
		}
		else {
			qi[m_bRunning] = false;
			SetArrayArray( g_adtQueryQueue, queryIndex, qi, csqiSize );
		}
		SPRAY_SQLConnect();
		return;
	}

	LogMessage( "NOTICE:\tSprayban for %s removed from database", targetClient, sAuth );

	QUERY_REMOVEFROMQUEUE(queryIndex);
}



public callbackGetBanQuery( Handle:db, Handle:q, const String:error[], any:queryUID )
{

}


public callbackGetBanMassQuery( Handle:db, Handle:q, const String:error[], any:queryUID )
{

}
















//-------------------------------------------
//
//
//			MENUS
//
//
//-------------------------------------------
















/********************************************
* Formats the given unix timestamp into a string containing the
* number of days, hours, minutes and seconds in the timestamp.
*
* @param 	utime 		Unix Timestamp to use.
* @param 	buffer 		Buffer to store the output.
* @param 	buflen 		Size of the output buffer.
* @noreturn
*/
FormatBanExpires( utime, String:buffer[], buflen )
{

	if ( utime == BANLENGTHPERMA ) {
		strcopy( buffer, buflen, "Permanant" );
		return;
	}

	new diff = utime - GetTime();
	//Debug_PrintToServer( "diff: %i", diff );

	if ( diff < 0 ) {
		Format( buffer, buflen, "ERRORTIME (%i)", diff );
		return;
	}

	decl String:aTimes[5][16],
		 aTimeVals[5];
	new iTimeCount = 0;

	// 604800, 86400, 3600, 60
	new weeks = RoundToFloor(diff / 604800.0);
	//Debug_PrintToServer( "weeks: %i", weeks );
	if ( weeks > 0 ) {		
		aTimeVals[iTimeCount] = weeks;
		strcopy( aTimes[iTimeCount], 8, (weeks == 1) ? "week" : "weeks" );
		iTimeCount++;
	}

	diff = diff % 604800;

	new days = RoundToFloor(diff / 86400.0);
	//Debug_PrintToServer( "days: %i", days );
	if ( days > 0 ) {
		aTimeVals[iTimeCount] = days;
		strcopy( aTimes[iTimeCount], 8, (days == 1) ? "day" : "days" );
		iTimeCount++;
	}

	diff = diff % 86400;

	new hours = RoundToFloor(diff / 3600.0);
	//Debug_PrintToServer( "hours: %i", hours );
	if ( hours > 0 ){
		aTimeVals[iTimeCount] = hours;
		strcopy( aTimes[iTimeCount], 8, (hours == 1) ? "hour" : "hours" );
		iTimeCount++;
	}
	diff = diff % 3600;

	new mins = RoundToFloor(diff / 60.0);
	//Debug_PrintToServer( "mins: %i", mins );
	if ( mins > 0 ) {
		aTimeVals[iTimeCount] = mins;
		strcopy( aTimes[iTimeCount], 8, (mins == 1) ? "minute" : "minutes" );
		iTimeCount++;
	}

	new secs = diff % 60;
	//Debug_PrintToServer( "secs: %i", secs );
	if ( secs > 0 ) {
		aTimeVals[iTimeCount] = secs;
		strcopy( aTimes[iTimeCount], 8, (secs == 1) ? "second" : "seconds" );
		iTimeCount++;
	}

	for (new i = 0; i<iTimeCount; i++ ) {		
		//Debug_PrintToServer( "i: %i", i );
		if ( i==0 ) {
			Format( buffer, buflen, "%i %s", aTimeVals[i], aTimes[i] );
			//Debug_PrintToServer( "buffer: %s", buffer );
			if ( (i+2) == iTimeCount ) {
				Format( buffer, buflen, "%s and", buffer );
				//Debug_PrintToServer( "buffer: %s", buffer );
			}
		}
		else if ( (i+1) == iTimeCount ) {
			Format( buffer, buflen, "%s %i %s", buffer, aTimeVals[i], aTimes[i] );
			//Debug_PrintToServer( "buffer: %s", buffer );
		}
		else {
			Format( buffer, buflen, "%s, %i %s", buffer, aTimeVals[i], aTimes[i] );
			//Debug_PrintToServer( "buffer: %s", buffer );
		}
	}
}














//-------------------------------------------
//
//
//			SPRAY MANAGER
//
//
//-------------------------------------------
















/********************************************
* Initialises all necessary variables for the plugin to function.
*/
SPRAY_Initialise()
{
	LogMessage( "NOTICE:\tInitialising..." );
	// (Re)create the timer that shows spray details.
	LogMessage( "NOTICE:\t\tspray detail timer" );
	if ( g_timerShowSprayDetails != INVALID_HANDLE )
		KillTimer( g_timerShowSprayDetails );	
	g_timerShowSprayDetails = CreateTimer( GetConVarFloat(cvTimerDelay), timerShowSprayDetails, INVALID_HANDLE, TIMER_REPEAT );

	// (Re)Create the HUD synchroniser if the cvar setting requires it.
	// Generate error if the current mod doesn't support this feature.
	LogMessage( "NOTICE:\t\thud synchroniser" );
	if (g_hGUI != INVALID_HANDLE)
		CloseHandle( g_hGUI );

	if ( GetConVarInt(cvDisplayMode) == 3 ) {
		g_hGUI = CreateHudSynchronizer();
		if (g_hGUI == INVALID_HANDLE) {
			LogError( "ERROR:\tsm_sprayman_displaymode = 3 (ShowHudText) is not available in this engine version. Defaulting to center say text." );
		}
		SetConVarInt(cvDisplayMode, 0 );
	}

	g_bSQLEnabled = GetConVarBool( cvSQLEnabled );

	// If SQL is enabled, connect to the database.
	if ( g_bSQLEnabled ) {
		SPRAY_SQLInitialise();
	}

	g_bEnabled = true;
}



/********************************************
* Cleans up all open resources and timers for shutdown.
*/
SPRAY_Uninitialise()
{
	if ( g_timerShowSprayDetails != INVALID_HANDLE ) {
		KillTimer( g_timerShowSprayDetails );
		g_timerShowSprayDetails = INVALID_HANDLE;
	}

	if ( g_hGUI != INVALID_HANDLE )
		CloseHandle( g_hGUI );

	if ( g_bSQLEnabled ) {
		SPRAY_SQLUninitialise();
	}
}



/********************************************
* Bans a player from being able to spray decals.
*
* @param client 		Admin client invoking this action.
* @param target 		Client to ban from spraying.
* @param reason 		Reason for the ban, if any.
* @param duration 		Duration for the ban, if defined.
*/
SPRAY_SprayBanClient( client, target, duration, const String:sReason[] )
{
	LogMessage( "\t\tSPRAY_SprayBanClient CALLED" );		

	// Ban user from spraying for this session.
	g_bSprayBanned[target] = true;

	if ( GetConVarBool(cvSQLEnabled) ) {

		if ( !g_bSQLReady || g_hSQL == INVALID_HANDLE ) {
			ReplyToCommand( client, "Woah there, spraybans isn't ready yet!" );
			return;
		}

		// Gather all the information required for storing into database.
		decl CSprayBanInfo:baninfo[ csbiSize ];
		decl String: sTargetAuth[ MAX_AUTH_LENGTH ],
		     String: sAdminAuth [ MAX_AUTH_LENGTH ],
		     String: sTargetName[ MAX_NAME_LENGTH ],
		     String: sAdminName [ MAX_NAME_LENGTH ];

		GetClientAuthString( target, sTargetAuth, MAX_AUTH_LENGTH );
		GetClientAuthString( client, sAdminAuth,  MAX_AUTH_LENGTH );
		GetClientName( target, sTargetName, MAX_NAME_LENGTH );
		GetClientName( client, sAdminName,  MAX_NAME_LENGTH );

		// Format the gathered information into a baninfo object.
		SPRAY_CreateBanInfo( baninfo, sTargetAuth, sTargetName, sAdminAuth, sAdminName, sReason, duration );

		// Fire off a query to check if the target already has a ban.
		SPRAY_SQLEnqueueCheckBanQuery( client, target, baninfo, CHECKQUERY_STORE );
	}	
}



/********************************************
* Unbans a player from being able to spray decals.
*
* @param client 		Client to unban from spraying.
*/
SPRAY_SprayUnbanClient( client, target )
{
	// Unban user from spraying for this session
	g_bSprayBanned[target] = false;

	if ( GetConVarBool(cvSQLEnabled) ) {
		if ( !g_bSQLEnabled || g_hSQL == INVALID_HANDLE ) {
			ReplyToCommand( client, "Woah there, spraybans isn't ready yet!" );
			return;
		}

		// Create baninfo
		// decl CSprayBanInfo:bi[ csbiSize ];

		decl String:sAuth[ MAX_AUTH_LENGTH ];
		GetClientAuthString( target, sAuth, MAX_AUTH_LENGTH );

		SPRAY_SQLEnqueueRemoveBanRecordQuery( client, target, sAuth );
	}
}


/********************************************
* "Removes" a spray from view by forcing the creation of a 
* new decal tempent at the maps origin. (0, 0, 0)
*
* @param client 		Client whose spray to remove.
*/
SPRAY_RemovePlayerDecal( client )
{
	SPRAY_CreatePlayerDecal( client, Float:{0.0, 0.0, 0.0} );
	g_bHasSprayed[client] = false;
}



/********************************************
* Creates a Player Decal TempEnt belonging to the given client
* and transmits it to all clients.
*
* @param client 		Client to own this Player Decal.G
* @param vecOrigin 		World position to create the decal at.
*/
SPRAY_CreatePlayerDecal( client, Float:vecOrigin[3] )
{
	TE_Start( "Player Decal" );
	TE_WriteNum( "m_nPlayer", client );
	TE_WriteVector( "m_vecOrigin", vecOrigin );
	TE_WriteNum( "m_nEntity", 0 );
	TE_SendToAll();

	g_flSprayLocs[client] = vecOrigin;

	if ( !g_bHasSprayed[client] )
		g_bHasSprayed[client] = true;
}



/********************************************
* Calculates if the given player is looking at a persons spray
* and returns true if so.
*
* @param client 		Client to check
* @return 				client id of the sprays owner, -1 if not found
*/
SPRAY_CalculateSprayVisible( client )
{
	// Debug_PrintToLog( false, "SPRAY_CalculateSprayVisible: client %i (%N)", client, client );

	decl Float:vecEndPos[3];
	// Get the vector of the location the player is aiming at. Skip if trace failed.
	if ( SPRAY_GetClientEyeTrace(client, vecEndPos) ) {

		// Debug_PrintToLog( false, "\tSPRAY_GetClientEyeTrace returned true" );

		decl Float:vecOrigin[3];
		GetClientEyePosition(client, vecOrigin);

		// Skip if client is too far away from a wall.
		if ( GetVectorDistance( vecEndPos, vecOrigin ) > GetConVarFloat( cvSprayDistance ) ) {
			// Debug_PrintToLog( false, "\tGetVectorDistance: %.2f > %.2f", GetVectorDistance(vecEndPos, vecOrigin), GetConVarFloat(cvSprayDistance) );
			return -1;
		}

		decl Float:vecSprayPos[3];
		// Iterate all sprays.
		for ( new j=1; j<=MaxClients; j++ ) {
			// Skip players who have never sprayed.
			if ( !g_bHasSprayed[j] )
				continue;

			// Debug_PrintToLog( false, "\tSpray %i (%s): %.2f %.2f %.2f", j, j, g_flSprayLocs[j][0], g_flSprayLocs[j][1], g_flSprayLocs[j][2] );
			vecSprayPos = g_flSprayLocs[j];

			// Check distance between endpos and spray location, fudge
			if ( GetVectorDistance( vecEndPos, vecSprayPos ) <= SPRAYFUDGEAMOUNT ) {
				// Debug_PrintToLog( false, "\t\tDistance: %.2f", flResult );
				return j;
			}
		}
	}
	return -1;
}



/********************************************
* Attempts to get the EndPos vector of a trace, originating from a clients eyes.
* 
* @param client 		Client to perform the trace.
* @param vecEndPos 		Vector to store the result EndPos.
* @return 				True if the trace hit a valid surface.
*/
bool:SPRAY_GetClientEyeTrace( client, Float:vecSprayPos[3] )
{
	//Debug_PrintToLog( false, "\tSPRAY_GetClientEyeTrace: client %i (%N)", client, client );

	new Float:vecEyeOrigin[3],
		Float:vecEyeAngles[3];

	GetClientEyePosition( client, vecEyeOrigin );
	GetClientEyeAngles( client,	vecEyeAngles );

	// Debug_PrintToLog( false, "\t\tvecEyeOrigin: %0.2f %0.2f %0.2f", vecEyeOrigin[0], vecEyeOrigin[1], vecEyeOrigin[2] );
	// Debug_PrintToLog( false, "\t\tvecEyeAngles: %0.2f %0.2f %0.2f", vecEyeAngles[1], vecEyeAngles[1], vecEyeAngles[2] );

	new Handle:hTrace = TR_TraceRayFilterEx( vecEyeOrigin, vecEyeAngles, MASK_SHOT, RayType_Infinite, filterNoPlayers );

	if ( TR_DidHit(hTrace) ) {
		TR_GetEndPosition( vecSprayPos, hTrace );
		CloseHandle(hTrace);

		// Debug_PrintToLog( false, "\t\thTrade did hit: %0.2f %0.2f %0.2f", vecEndPos[0], vecEndPos[1], vecEndPos[2] );

		return true;
	}
	CloseHandle(hTrace);

	// Debug_PrintToLog( false, "\t\tTrace NO HIT!" );

	return false;
}
public bool:filterNoPlayers(entity, contentsmask) {
	return entity > MaxClients;
}



/********************************************
* Prints spray owner name to clients HUD
*
* @param 	client 			Client whose hud to draw on.
* @param 	target 			Client to use for the name.
* @noreturn
*/
SPRAY_DisplaySprayName( client, target )
{
	if ( !Client_IsIngame(client) )
		return;

	if ( !Client_IsIngame( target ) )
		return;

	decl String:sBuffer[MAX_MESSAGE_LENGTH];
	Format( sBuffer, sizeof(sBuffer), "Sprayed by: %N", target );

	switch( GetConVarInt(cvDisplayMode) ) {
		case 0: PrintCenterText( client, sBuffer );
		case 1: Client_PrintKeyHintText( client, sBuffer );
		case 2: {
			Client_PrintHintText( client, sBuffer );
			StopSound(client, SNDCHAN_STATIC, "UI/hint.wav");
		}
		case 3: {
			if ( g_hGUI != INVALID_HANDLE )
			{				
				SetHudTextParams( HUDMSG_POSX, HUDMSG_POSY, GetConVarFloat(cvTimerDelay), HUDMSG_RED, HUDMSG_GREEN, HUDMSG_BLUE, HUDMSG_ALPHA, 0, 0.1, 0.1, 0.1 );
				ShowSyncHudText( client, g_hGUI, sBuffer );
			}
		}
	}
}


/********************************************
* Takes loose data describing a new ban and puts it into a baninfo record
*
* @param 	bi 				BanInfo array to store into.
* @param 	targetAuth 		SteamID of the target client.
* @param 	targetName 		Name of the target client.
* @param 	adminAuth 		SteamID of the admin that initiated this ban.
* @param 	adminName 		Name of the admin.
* @param 	iDuration 		Length of the ban, in seconds.
* @noreturn
*/
SPRAY_CreateBanInfo( CSprayBanInfo:bi[csbiSize], 
							const String:targetAuth[], 
							const String:targetName[],
							const String:adminAuth[],
							const String:adminName[],
							const String:sReason[],
							const iDuration
					)
{
	new iExpireTime = 0;

	if ( iDuration > 0 ) {
		iExpireTime = GetTime() + iDuration;	
	}
	bi[expires] = iExpireTime;

	strcopy( bi[auth], MAX_AUTH_LENGTH, targetAuth );
	strcopy( bi[name], MAX_NAME_LENGTH, targetName );

	strcopy( bi[adminauth], MAX_AUTH_LENGTH, adminAuth );
	strcopy( bi[adminname], MAX_NAME_LENGTH, adminName );

	strcopy( bi[reason], MAX_REASON_LENGTH, sReason );
}
















//-------------------------------------------
//
//
//			MISCELLENEOUS
//
//
//-------------------------------------------
















/********************************************
* Converts a number into it's ordinal abbreviation equivalent.
* 
* @param num 		Number to convert.
* @param sBuffer 	String buffer to store the result.
* @param buflen 	Size of the buffer.
* @noreturn 
*/
// static StNdRdNum( num, String:sBuffer[], buflen )
// {
// 	if ( num <= 0 )
// 		IntToString( num, sBuffer, buflen );
// 	else if ( num >= 11 && num <= 13 )
// 		Format( sBuffer, buflen, "%ith", num );
// 	else {
// 		new mod = num % 10;
// 		switch( mod ) {
// 			case 1: Format( sBuffer, buflen, "%ist", num );
// 			case 2: Format( sBuffer, buflen, "%ind", num );
// 			case 3: Format( sBuffer, buflen, "%ird", num );
// 			default: Format( sBuffer, buflen, "%ith", num );
// 		}
// 	}
// }



/********************************************
* Generates a random number between between 2 values
*
* @param 	min 	Low point of the random number range
* @param 	max 	High point of the random number range
* @return 			randomly generated integer between min and max
*/
// static randNum( const min, const max )
// {
// 	new num = GetURandomInt();

// 	if ( num < min )
// 		return (num *= 2);
// 	if ( num > max )
// 		return RoundToCeil(num *= 0.5);
// 	return num;
// }